package com.test;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedTransferQueue;
import java.util.zip.GZIPInputStream;

/**

Arduino: ATMEGA328P-PU
===================================
void setup() {
  Serial.begin(115200);
}
void loop() {
  int sensorValue = analogRead(A0);
  Serial.print("v=");
  Serial.println(sensorValue);
  delay(5);
}
===================================
http://sekarlangit.com/arduino-differential-gain.php



 */
public class ArduinoOscillograph {
  static interface ValSource{
    int next() ;
  }

  static class HanningWindowSource implements ValSource{
    // http://www.robots.ox.ac.uk/~sjrob/Teaching/SP/l6.pdf
    ValSource base;
    final int N=3;
    int[] prev = new int[2*N-1];
    double[] koef = new double[2*N-1];
    HanningWindowSource(ValSource base){
      this.base=base;

      double alpha=0.5; // Hanning
      double sum=0;
      for( int k=-N+1; k<N; k++ ){
        double v = alpha + (1-alpha)*Math.cos(k*Math.PI/N);
        koef[k+N-1] = v;
        sum += v;
      }
      for( int i=0; i<koef.length; i++ ){
        koef[i] /= sum;
      }

      for( int i=0; i<prev.length-1; i++ ){
        shiftR();
        prev[0]=base.next();
      }
    }
    
    void shiftR(){
      for( int i = prev.length-1; i>0; i-- ){
        prev[i]=prev[i-1];
      }
    }

    @Override
    public int next() {
      shiftR();
      prev[0]=base.next();

      //return (prev[0] + 2*prev[1] + prev[2])/4;
      double sum=0;
      for( int i=0; i<koef.length; i++ ){
        sum += koef[i]*prev[i];
      }
      return (int)sum;
    }
  }

  static class FileSource implements ValSource{
    BufferedReader br;

    FileSource(){
      try {
        br = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(
            "/home/denny/proj/scala-agent3/2013/pulseFile.txt.gz"))));
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public int next() {
      //Thread.sleep(5);
      try {
        Thread.sleep(5);
      } catch (InterruptedException e) {
      }
      try {
        return Integer.parseInt(br.readLine());
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

  static class SerialPortSource implements ValSource, Runnable{
    BlockingQueue<Integer> q = new LinkedTransferQueue<>(); 
    SerialPortSource() throws IOException{
      new Thread(this).start();
    }

    private void go() throws IOException {
      String dev = null;
      for(int i=0;i<10;i++){
        dev = "/dev/ttyUSB"+i;
        if( new File(dev).exists() ){
          break;
        }
      }
      System.err.println("using "+dev);
      int speed = 115200;//9600;
      Process process = Runtime.getRuntime().exec(new String[]{
          "bash", "-c", "stty -F "+dev+" "+speed+" cs8 cread clocal; exec 3<> "+dev+"; tail -s 0.01 -f <&3"
      });
      BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
      String line;
      while((line=br.readLine())!=null){
        //System.out.println(line);
        String start = "v=";
        if( line.startsWith(start) ){
          int val = Integer.parseInt(line.substring(start.length()));
          q.add(val);
        }
      }
      System.out.println("END");
    }

    @Override
    public int next() {
      try {
        return q.take();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public void run() {
      try {
        go();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }
  
  public static void main(String[] args) throws Exception{
    ValSource valSource = new SerialPortSource();
    //ValSource valSource = new FileSource();
    for( int i=0; i<3; i++ ){
      valSource = new HanningWindowSource(valSource);
    }

    Thread.currentThread().setName("main-thread000");
    final int SIZE=700;
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setSize(SIZE+50, SIZE+50);
    f.setLayout(new BorderLayout());
    Canvas canvas =  new Canvas();
    f.add(canvas, BorderLayout.CENTER);

    JPanel panel = new JPanel(new FlowLayout());
    JLabel labelHeartRate = new JLabel("Heart rate: 000");
    panel.add(labelHeartRate);
    ValsSpreadGraph valsSpreadGraph = new ValsSpreadGraph(50,150,25,   10);
    panel.add(valsSpreadGraph);
    f.add(panel, BorderLayout.SOUTH);


    f.setVisible(true);
    
    Graphics g = canvas.getGraphics();
    

    int x=0;
    int xall=0;
    long t0=System.currentTimeMillis();
    int[] dataLine = new int[SIZE*16];
    int dataLinePos=0;
    PulseDetector detector = new PulseDetector();
    FileWriter pulseFile = new FileWriter("/tmp/pulseFile.txt");
    for(;;){
        int val = valSource.next();
        detector.add(val);
        System.out.println((System.currentTimeMillis()-t0)+" "+xall+" "+ val+"  "+val/1024.*5+" volt");
        pulseFile.write(""+val+'\n');
        val = digitalZoom(val);

        g.setColor(Color.WHITE);
        g.drawLine(x+1,0,x+1,SIZE);

        g.setColor(Color.WHITE);
        g.drawLine(x,0,x,SIZE);

        g.setColor(Color.BLUE);
        g.drawLine(x,val*SIZE/1024,x,SIZE);
        //g.drawLine(x,val*SIZE/1024,x,val*SIZE/1024);
      
        if( detector.isDeletected() ){
          int pulsePos = (x + SIZE + detector.detectedPos()) % SIZE;
          g.setColor(Color.GREEN);
          g.drawLine(pulsePos,0,pulsePos,SIZE);
          Double heartRate = detector.getHeartRate();
          labelHeartRate.setText("Heart rate: "+
              (!detector.isStableHeartRate() ? "000" : ""+heartRate.intValue()));
          if( heartRate!=null ){
            valsSpreadGraph.addVal(heartRate.intValue());
          }
        }
        
        x = (x + 1) % SIZE;
        xall++;

        dataLine[dataLinePos] = val;
        dataLinePos = (dataLinePos + 1)%dataLine.length;
/*
        if( dataLinePos==0 ){
          // end of line
          double[] freq = new App().fft(dataLine);
          for( int i=0; i<freq.length; i++ ){
            System.out.println("freq "+i+" "+freq[i]);
          }
        }
        */
      
    }
  }
  
  static class PulseEvent{
	long time = System.currentTimeMillis();
    boolean absMax=true;
    boolean absMin=true;
    int center;
    int bandMax = Integer.MIN_VALUE;
    int bandMin = Integer.MAX_VALUE;
    int vcenter;
    void bandMinMax(int v){
      bandMin = Math.min(bandMin, v);
      bandMax = Math.max(bandMax, v);
    }
    int bandH(){
      return bandMax - bandMin;
    }
    void absMinMaxVerify(int iCenterShift, int v){
      if( iCenterShift<0 && v>=vcenter ){ // 'center' must be first of maximal points 
        absMax=false;
      }
      if( iCenterShift>0 && v>vcenter ){
        absMax=false;
      }
      if( iCenterShift<0 && v<=vcenter ){ // 'center' must be first of maximal points
        absMin=false;
      }
      if( iCenterShift>0 && v<vcenter ){
        absMin=false;
      }
    }
  }
  
  static class PulseDetector{
    List<Integer> allVals = new ArrayList<>();
    List<PulseEvent> pulseEvents = new ArrayList<>();
    int VALS_PER_SEC=200;
    int BAND=40; // 200 millis to both sides
    //int PULSE=5; // 25 millis to both sides
    int PULSE=7;
    int MIN_RESOLUTION=3;
    int MAX_RESOLUTION=50;

    ArrayList<Double> heartRates = new ArrayList<>();
        
    void add(int val){
      allVals.add(val);
    }
    int detectedPos(){
      return -BAND;
    }
    boolean apolar(PulseEvent a, PulseEvent b){
      return a.absMin!=b.absMin || a.absMax!=b.absMax;
    }
    Double getHeartRate(){
      if( heartRates.isEmpty() ){
        return null;
      }
      return heartRates.get(heartRates.size()-1);
    }
    boolean isStableHeartRate(){
      if( heartRates.size()<2 ){
        return false;
      }
      Double v0 = heartRates.get(heartRates.size()-1);
      Double v1 = heartRates.get(heartRates.size()-2);
      if( v0==null || v1==null ){
        return false;
      }
      return v0 < 1.5 * v1 && v1 < 1.5 * v0;
    }

    private Double heartRateCalc() {
      if( pulseEvents.size()<2 ){
        return null;
      }
      PulseEvent now = pulseEvents.get(pulseEvents.size()-1);
      PulseEvent last = pulseEvents.get(pulseEvents.size()-2);
      if( apolar(now, last) ){
        return null;
      }
      //double rate = 200 * 60. / (now.center - last.center);
      double rate = 1000 * 60. / (now.time - last.time);
      if( rate < 20 || rate>300 ){
        return null;
      }
      return rate;
    }

    boolean isDeletected(){
      if( allVals.size() > BAND*2 ){
        PulseEvent e = new PulseEvent();
        e.center = allVals.size() - BAND - 1;
        e.vcenter = allVals.get(e.center);
        //int slopeMax=0;
        for( int i=-BAND; i<=BAND; i++ ){
          int v = allVals.get(e.center+i);
          e.absMinMaxVerify(i, v);
          if( i< -PULSE || i > PULSE ){
            e.bandMinMax(v);
          }else if( i!=0 ){
            //slopeMax = Math.max(slopeMax, Math.abs((v - vcenter)/i));
          }
        }
        //boolean fastSlope = (vcenter - bandMin) < MAX_T_SLOPE * slopeMax;
        int pulseH;
        boolean highImpulse;
        if( e.absMax ){
          pulseH = e.vcenter - e.bandMax;
          highImpulse = pulseH > e.bandH() * 0.3
             && pulseH > MIN_RESOLUTION && pulseH < MAX_RESOLUTION;
        }else if( e.absMin ){
          pulseH = e.vcenter - e.bandMin;
          highImpulse = -pulseH > e.bandH() * 0.3
              && -pulseH > MIN_RESOLUTION && -pulseH < MAX_RESOLUTION;
        }else{
          return false;
        }
        if( highImpulse ){
          if( pulseEvents.size()>0 ){
            PulseEvent prev = pulseEvents.get(pulseEvents.size()-1);
            if( apolar(prev, e) && e.center - prev.center < 2 * VALS_PER_SEC ){
              return false;
            }
          }
          System.out.println("highImpulse: "+e.vcenter + " "+e.bandMax
              +" "+e.bandH()+" pulseH="+pulseH);
          pulseEvents.add(e);
          Double hrate = heartRateCalc();
          heartRates.add(hrate);
          if( hrate!=null ){
            System.out.println("                    heart rate: "+hrate+
              (isStableHeartRate() ? "   STABLE":""));
          }

          return true;
        }
      }
      return false;
    }
  }
  
  static int digitalZoom(int val){
      int VAL_MIN=450;
      int VAL_MAX=550;
      if( val<=VAL_MIN ){
    	  return 0;
      }
      if( val>=VAL_MAX ){
    	  return 1024;
      }
      return (val-VAL_MIN)*1024/(VAL_MAX-VAL_MIN);
  }
}
